package com.zxjbyte.yiyi.framework.redis.lock;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ObjectUtil;
import com.google.common.collect.Maps;
import com.zxjbyte.yiyi.framework.redis.config.RedisProperties;
import com.zxjbyte.yiyi.framework.redis.lock.enums.RLockType;
import com.zxjbyte.yiyi.framework.redis.lock.executor.FairRLockExecutor;
import com.zxjbyte.yiyi.framework.redis.lock.executor.IRLockExecutor;
import com.zxjbyte.yiyi.framework.redis.lock.executor.ReentrantRLockExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 基于redis分布式锁的模板方法
 *
 * @Author zhangxingjia
 * @Date 2023/4/14 17:18
 * @Version: 1.0
 */
@Slf4j
@SuppressWarnings("rawtypes")
public class RLockTemplate implements InitializingBean {

    private final Map<RLockType, IRLockExecutor> executorMap = Maps.newLinkedHashMap();

    private final RedisProperties.RLookProperties properties;
    private final List<IRLockExecutor> executors;

    public RLockTemplate(RedisProperties properties, List<IRLockExecutor> executors){
        this.properties = properties.getRedisLock();
        this.executors = executors;
    }

    public RLockInfo lock(String key){
        return lock(key, -1L, 0L, TimeUnit.MILLISECONDS, RLockType.Reentrant);
    }

    /**
     * 加锁
     * @param key
     * @param waitTime
     * @param leaseTime
     * @param type
     * @return
     */
    public RLockInfo lock(String key, long waitTime, long leaseTime, TimeUnit unit, RLockType type){
        if(waitTime < 0){
            waitTime = properties.getWaitTime();
        }
        waitTime = toMillis(unit, waitTime);
        if(leaseTime <= 0){
            leaseTime = properties.getLeaseTime();
        }
        leaseTime = toMillis(unit, leaseTime);
        long retryInterval = properties.getRetryInterval();
        IRLockExecutor lockExecutor = getExecutor(type);
        log.debug("The current lock used type: [{}], key: [{}]", type.name(), key);
        long start = System.currentTimeMillis();
        int acquireCount = 0;
        do {
            acquireCount++;
            Object lockInst = lockExecutor.acquire(key, waitTime, leaseTime);
            if (ObjectUtil.isNotNull(lockInst)) {
                return RLockInfo.builder().lockKey(key).waitTime(waitTime).leaseTime(leaseTime)
                        .lockInst(lockInst).lockExecutor(lockExecutor).acquireCount(acquireCount).build();
            }
            ThreadUtil.sleep(retryInterval, TimeUnit.MILLISECONDS);
        } while (System.currentTimeMillis() - start < waitTime);
        return null;
    }

    @SuppressWarnings("unchecked")
    public boolean releaseLock(RLockInfo lockInfo) {
        if (ObjectUtil.isNull(lockInfo)) {
            return false;
        }
        return lockInfo.getLockExecutor().release(lockInfo.getLockKey(), lockInfo.getLockInst());
    }

    private long toMillis(TimeUnit unit, long time){
        if(ObjectUtil.equals(TimeUnit.MILLISECONDS, unit)){
            return time;
        }
        return unit.toMillis(time);
    }

    private IRLockExecutor getExecutor(RLockType type) {
        final IRLockExecutor lockExecutor = executorMap.get(type);
        Assert.notNull(lockExecutor, type.name()+ " type executor is null");
        return lockExecutor;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.isTrue(properties.getWaitTime() >= 0, "waitTime must least 0");
        Assert.isTrue(properties.getLeaseTime() >= -1, "leaseTime must lease -1");
        Assert.isTrue(properties.getRetryInterval() >= 0, "retryInterval must more than 0");
        Assert.notEmpty(executors, "executors must have at least one");
        executors.forEach(executor -> {
            RLockType rLockType = null;
            if(executor instanceof ReentrantRLockExecutor){
                rLockType = RLockType.Reentrant;
            } else if(executor instanceof FairRLockExecutor){
                rLockType = RLockType.Fair;
            }
            executorMap.put(rLockType, executor);
        });

    }
}
